除了死鎖,還有其他可能會讓程式卡住,一個是「活鎖(livelock)」、另一個是「飢餓(starvation)」。
有別於「死鎖」是複數個執行緒都持有部分資源、等待其他執行緒釋出另一部分的資源而卡死,「活鎖」則是複數個執行緒都互相釋出資源,導致彼此都沒有足夠的資源完成工作,一樣會卡著無法運行。
下列程式碼即為「活鎖」範例:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex spoon1, spoon2; // 各自命名為 spoon1 與 spoon2 的互斥鎖
void worker(const char* name, std::mutex& first, std::mutex& second) {
while (true) {
first.lock(); // 執行緒先拿第一把湯匙
std::cout << name << " got first spoon\n";
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 等待一秒鐘
if (!second.try_lock()) { // 如果拿不到另一把湯匙
std::cout << name << " gives up to be polite\n";
first.unlock(); // 就把手上的湯匙給放出來
continue;
}
// 若有一執行緒順利拿到兩把湯匙,就執行這裡
std::cout << name << " got both spoons and can eat\n";
second.unlock();
first.unlock();
return;
}
std::cout << name << " failed after many polite retries\n";
}
int main() {
std::thread t1(worker, "A", std::ref(spoon1), std::ref(spoon2));
std::thread t2(worker, "B", std::ref(spoon2), std::ref(spoon1));
t1.join();
t2.join();
}
在此範例中,第一個執行緒、也就是取名為 A 的 worker 先取 spoon1 再取 spoon2;第二個執行緒、也就是取名為 B 的 worker 先取 spoon2 再取 spoon1。
對於兩個執行緒來說,都想要拿到兩把湯匙才可以吃飯,但在 if (!second.try_lock()) 階段,會看如果拿不到另一把湯匙,就把手上的湯匙給放出來,但兩執行緒互相讓來讓去,最後都無法完整持有兩把湯匙,也就無法吃飯。
這樣的程式碼執行結果如下:
A got first spoon
B got first spoon
A gives up to be polite
A got first spoon
B gives up to be polite
...
可發現兩執行緒都能取得第一把湯匙,但拿到之後又一直把手上湯匙讓出來,因此無法順利拿到兩把湯匙、進到 std::cout << name << " got both spoons and can eat\n"; 區塊。
如 Day 26 講死結的鎖法,從 C++ 17 開始可用 std::scoped_lock 協調,將所有的鎖統一管理,並透過其內建機制,確保不發生活鎖:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex spoon1, spoon2; // 各自命名為 spoon1 與 spoon2 的互斥鎖
void worker(const char* name, std::mutex& first, std::mutex& second) {
// 把 first 與 second 都交給 scoped_lock 管理
std::scoped_lock lock(first, second);
// 兩執行緒皆可順利抵達這行
std::cout << name << " got both spoons and can eat\n";
return;
}
int main() {
std::thread t1(worker, "A", std::ref(spoon1), std::ref(spoon2));
std::thread t2(worker, "B", std::ref(spoon2), std::ref(spoon1));
t1.join();
t2.join();
}
「飢餓」則是指某一執行緒一直得不到資源,只能在旁邊等,例如以下範例:
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <string>
std::mutex resourceMutex; // 名為 resourceMutex 的互斥鎖
bool isRunning = true;
void worker(std::string name, int holdTimeMs, int restTimeMs, int& successCount) {
while (isRunning) {
// 排隊取鎖並上鎖
resourceMutex.lock();
// 成功取得資源,開始工作
successCount++;
std::cout << name << " Got the lock " << successCount << " times.\n";
std::this_thread::sleep_for(std::chrono::milliseconds(holdTimeMs));
// 釋放鎖
resourceMutex.unlock();
// 釋放鎖後的喘息時間
if (restTimeMs > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(restTimeMs));
}
}
}
int main() {
int greedyCount = 0;
int starvedCount = 0;
std::cout << "Start\n";
// 貪婪工人:拿到鎖後霸佔 500 毫秒,釋放後「完全不休息 (0ms)」立刻又想去排隊
std::thread t1(worker, "Greedy worker", 500, 0, std::ref(greedyCount));
// 可憐工人:拿到鎖後只用 10 毫秒,釋放後還要休息 10 毫秒
std::thread t2(worker, "Poor worker", 10, 10, std::ref(starvedCount));
// 讓遊戲跑 5 秒鐘後停止
std::this_thread::sleep_for(std::chrono::seconds(5));
isRunning = false;
t1.join();
t2.join();
std::cout << "\n============= Final Result =============\n";
std::cout << "Greedy worker got the lock " << greedyCount << " times.\n";
std::cout << "Poor workder got the lock " << starvedCount << " times.\n";
return 0;
}
這裡的「貪婪工人 t1」每次拿到鎖就霸佔長達 500 毫秒,且釋放後完全不休息就又去搶鎖;「可憐工人 t2」拿到鎖後只能用 10 毫秒,釋放後還再休息 10 豪秒,導致鎖長時間被「貪婪工人 t1」佔據,執行結果如下:
Start
Greedy worker Got the lock 1 times.
Greedy worker Got the lock 2 times.
Greedy worker Got the lock 3 times.
Greedy worker Got the lock 4 times.
Greedy worker Got the lock 5 times.
Greedy worker Got the lock 6 times.
Greedy worker Got the lock 7 times.
Greedy worker Got the lock 8 times.
Greedy worker Got the lock 9 times.
Greedy worker Got the lock 10 times.
Poor worker Got the lock 1 times.
============= Final Result =============
Greedy worker got the lock 10 times.
Poor workder got the lock 1 times.
跑了 5 秒鐘,「貪婪工人 t1」搶到 10 次、「可憐工人 t2」卻只拿到 1 次,我們說「可憐工人 t2」面臨拿不到資源的飢餓情況。
飢餓的解法很多,以本例而言,最簡單的方法是讓「貪婪工人 t1」在釋放鎖後,也加上休息時間,這邊以休息 10 毫秒計算:
std::thread t1(worker, "Greedy worker", 500, 10, std::ref(greedyCount));
這樣鎖就不會永遠被「貪婪工人 t1」霸佔了,執行結果如下:
Start
Greedy worker Got the lock 1 times.
Poor worker Got the lock 1 times.
Greedy worker Got the lock 2 times.
Poor worker Got the lock 2 times.
Greedy worker Got the lock 3 times.
Poor worker Got the lock 3 times.
Greedy worker Got the lock 4 times.
Poor worker Got the lock 4 times.
Greedy worker Got the lock 5 times.
Poor worker Got the lock 5 times.
Greedy worker Got the lock 6 times.
Poor worker Got the lock 6 times.
Greedy worker Got the lock 7 times.
Poor worker Got the lock 7 times.
Greedy worker Got the lock 8 times.
Poor worker Got the lock 8 times.
Greedy worker Got the lock 9 times.
Poor worker Got the lock 9 times.
Greedy worker Got the lock 10 times.
Poor worker Got the lock 10 times.
============= Final Result =============
Greedy worker got the lock 10 times.
Poor workder got the lock 10 times.
兩工人各自都成功取得鎖 10 次。